#ifndef RANG_DOT_HPP
#define RANG_DOT_HPP

#if defined(__unix__) || defined(__unix) || defined(__linux__)
#define OS_LINUX
#elif defined(WIN32) || defined(_WIN32) || defined(_WIN64)
#define OS_WIN
#elif defined(__APPLE__) || defined(__MACH__)
#define OS_MAC
#else
#error Unknown Platform
#endif

#if defined(OS_LINUX) || defined(OS_MAC)
#include <unistd.h>

#elif defined(OS_WIN)

#if defined(_WIN32_WINNT) && (_WIN32_WINNT < 0x0600)
#error                                                                         \
  "Please include rang.hpp before any windows system headers or set _WIN32_WINNT at least to _WIN32_WINNT_VISTA"
#elif !defined(_WIN32_WINNT)
#define _WIN32_WINNT _WIN32_WINNT_VISTA
#endif

#include <windows.h>
#include <io.h>
#include <memory>

// Only defined in windows 10 onwards, redefining in lower windows since it
// doesn't gets used in lower versions
// https://docs.microsoft.com/en-us/windows/console/getconsolemode
#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
#endif

#endif

#include <algorithm>
#include <atomic>
#include <cstdlib>
#include <cstring>
#include <iostream>

namespace rang {

    /* For better compability with most of terminals do not use any style settings
     * except of reset, bold and reversed.
     * Note that on Windows terminals bold style is same as fgB color.
     */
    enum class style {
        reset = 0,
        bold = 1,
        dim = 2,
        italic = 3,
        underline = 4,
        blink = 5,
        rblink = 6,
        reversed = 7,
        conceal = 8,
        crossed = 9
    };

    enum class fg {
        black = 30,
        red = 31,
        green = 32,
        yellow = 33,
        blue = 34,
        magenta = 35,
        cyan = 36,
        gray = 37,
        reset = 39
    };

    enum class bg {
        black = 40,
        red = 41,
        green = 42,
        yellow = 43,
        blue = 44,
        magenta = 45,
        cyan = 46,
        gray = 47,
        reset = 49
    };

    enum class fgB {
        black = 90,
        red = 91,
        green = 92,
        yellow = 93,
        blue = 94,
        magenta = 95,
        cyan = 96,
        gray = 97
    };

    enum class bgB {
        black = 100,
        red = 101,
        green = 102,
        yellow = 103,
        blue = 104,
        magenta = 105,
        cyan = 106,
        gray = 107
    };

    enum class control {  // Behaviour of rang function calls
        Off = 0,  // toggle off rang style/color calls
        Auto = 1,  // (Default) autodect terminal and colorize if needed
        Force = 2  // force ansi color output to non terminal streams
    };
    // Use rang::setControlMode to set rang control mode

    enum class winTerm {  // Windows Terminal Mode
        Auto = 0,  // (Default) automatically detects wheter Ansi or Native API
        Ansi = 1,  // Force use Ansi API
        Native = 2  // Force use Native API
    };
    // Use rang::setWinTermMode to explicitly set terminal API for Windows
    // Calling rang::setWinTermMode have no effect on other OS

    namespace rang_implementation {

        inline std::atomic<control>& controlMode() noexcept
        {
            static std::atomic<control> value(control::Auto);
            return value;
        }

        inline std::atomic<winTerm>& winTermMode() noexcept
        {
            static std::atomic<winTerm> termMode(winTerm::Auto);
            return termMode;
        }

        inline bool supportsColor() noexcept
        {
#if defined(OS_LINUX) || defined(OS_MAC)

            static const bool result = [] {
                const char* Terms[]
                    = { "ansi",    "color",  "console", "cygwin", "gnome",
                        "konsole", "kterm",  "linux",   "msys",   "putty",
                        "rxvt",    "screen", "vt100",   "xterm" };

                const char* env_p = std::getenv("TERM");
                if (env_p == nullptr) {
                    return false;
                }
                return std::any_of(std::begin(Terms), std::end(Terms),
                    [&](const char* term) {
                        return std::strstr(env_p, term) != nullptr;
                    });
            }();

#elif defined(OS_WIN)
            // All windows versions support colors through native console methods
            static constexpr bool result = true;
#endif
            return result;
        }

#ifdef OS_WIN


        inline bool isMsysPty(int fd) noexcept
        {
            // Dynamic load for binary compability with old Windows
            const auto ptrGetFileInformationByHandleEx
                = reinterpret_cast<decltype(&GetFileInformationByHandleEx)>(
                    GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")),
                        "GetFileInformationByHandleEx"));
            if (!ptrGetFileInformationByHandleEx) {
                return false;
            }

            HANDLE h = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
            if (h == INVALID_HANDLE_VALUE) {
                return false;
            }

            // Check that it's a pipe:
            if (GetFileType(h) != FILE_TYPE_PIPE) {
                return false;
            }

            // POD type is binary compatible with FILE_NAME_INFO from WinBase.h
            // It have the same alignment and used to avoid UB in caller code
            struct MY_FILE_NAME_INFO {
                DWORD FileNameLength;
                WCHAR FileName[MAX_PATH];
            };

            auto pNameInfo = std::unique_ptr<MY_FILE_NAME_INFO>(
                new (std::nothrow) MY_FILE_NAME_INFO());
            if (!pNameInfo) {
                return false;
            }

            // Check pipe name is template of
            // {"cygwin-","msys-"}XXXXXXXXXXXXXXX-ptyX-XX
            if (!ptrGetFileInformationByHandleEx(h, FileNameInfo, pNameInfo.get(),
                sizeof(MY_FILE_NAME_INFO))) {
                return false;
            }
            std::wstring name(pNameInfo->FileName, pNameInfo->FileNameLength / sizeof(WCHAR));
            if ((name.find(L"msys-") == std::wstring::npos
                && name.find(L"cygwin-") == std::wstring::npos)
                || name.find(L"-pty") == std::wstring::npos) {
                return false;
            }

            return true;
        }

#endif

        inline bool isTerminal(const std::streambuf* osbuf) noexcept
        {
            using std::cerr;
            using std::clog;
            using std::cout;
#if defined(OS_LINUX) || defined(OS_MAC)
            if (osbuf == cout.rdbuf()) {
                static const bool cout_term = isatty(fileno(stdout)) != 0;
                return cout_term;
            }
            else if (osbuf == cerr.rdbuf() || osbuf == clog.rdbuf()) {
                static const bool cerr_term = isatty(fileno(stderr)) != 0;
                return cerr_term;
            }
#elif defined(OS_WIN)
            if (osbuf == cout.rdbuf()) {
                static const bool cout_term
                    = (_isatty(_fileno(stdout)) || isMsysPty(_fileno(stdout)));
                return cout_term;
            }
            else if (osbuf == cerr.rdbuf() || osbuf == clog.rdbuf()) {
                static const bool cerr_term
                    = (_isatty(_fileno(stderr)) || isMsysPty(_fileno(stderr)));
                return cerr_term;
            }
#endif
            return false;
        }

        template <typename T>
        using enableStd = typename std::enable_if<
            std::is_same<T, rang::style>::value || std::is_same<T, rang::fg>::value
            || std::is_same<T, rang::bg>::value || std::is_same<T, rang::fgB>::value
            || std::is_same<T, rang::bgB>::value,
            std::ostream&>::type;


#ifdef OS_WIN

        struct SGR {  // Select Graphic Rendition parameters for Windows console
            BYTE fgColor;  // foreground color (0-15) lower 3 rgb bits + intense bit
            BYTE bgColor;  // background color (0-15) lower 3 rgb bits + intense bit
            BYTE bold;  // emulated as FOREGROUND_INTENSITY bit
            BYTE underline;  // emulated as BACKGROUND_INTENSITY bit
            BOOLEAN inverse;  // swap foreground/bold & background/underline
            BOOLEAN conceal;  // set foreground/bold to background/underline
        };

        enum class AttrColor : BYTE {  // Color attributes for console screen buffer
            black = 0,
            red = 4,
            green = 2,
            yellow = 6,
            blue = 1,
            magenta = 5,
            cyan = 3,
            gray = 7
        };

        inline HANDLE getConsoleHandle(const std::streambuf* osbuf) noexcept
        {
            if (osbuf == std::cout.rdbuf()) {
                static const HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
                return hStdout;
            }
            else if (osbuf == std::cerr.rdbuf() || osbuf == std::clog.rdbuf()) {
                static const HANDLE hStderr = GetStdHandle(STD_ERROR_HANDLE);
                return hStderr;
            }
            return INVALID_HANDLE_VALUE;
        }

        inline bool setWinTermAnsiColors(const std::streambuf* osbuf) noexcept
        {
            HANDLE h = getConsoleHandle(osbuf);
            if (h == INVALID_HANDLE_VALUE) {
                return false;
            }
            DWORD dwMode = 0;
            if (!GetConsoleMode(h, &dwMode)) {
                return false;
            }
            dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
            if (!SetConsoleMode(h, dwMode)) {
                return false;
            }
            return true;
        }

        inline bool supportsAnsi(const std::streambuf* osbuf) noexcept
        {
            using std::cerr;
            using std::clog;
            using std::cout;
            if (osbuf == cout.rdbuf()) {
                static const bool cout_ansi
                    = (isMsysPty(_fileno(stdout)) || setWinTermAnsiColors(osbuf));
                return cout_ansi;
            }
            else if (osbuf == cerr.rdbuf() || osbuf == clog.rdbuf()) {
                static const bool cerr_ansi
                    = (isMsysPty(_fileno(stderr)) || setWinTermAnsiColors(osbuf));
                return cerr_ansi;
            }
            return false;
        }

        inline const SGR& defaultState() noexcept
        {
            static const SGR defaultSgr = []() -> SGR {
                CONSOLE_SCREEN_BUFFER_INFO info;
                WORD attrib = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE;
                if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE),
                    &info)
                    || GetConsoleScreenBufferInfo(GetStdHandle(STD_ERROR_HANDLE),
                        &info)) {
                    attrib = info.wAttributes;
                }
                SGR sgr = { 0, 0, 0, 0, FALSE, FALSE };
                sgr.fgColor = attrib & 0x0F;
                sgr.bgColor = (attrib & 0xF0) >> 4;
                return sgr;
            }();
            return defaultSgr;
        }

        inline BYTE ansi2attr(BYTE rgb) noexcept
        {
            static const AttrColor rev[8]
                = { AttrColor::black,  AttrColor::red,  AttrColor::green,
                    AttrColor::yellow, AttrColor::blue, AttrColor::magenta,
                    AttrColor::cyan,   AttrColor::gray };
            return static_cast<BYTE>(rev[rgb]);
        }

        inline void setWinSGR(rang::bg col, SGR& state) noexcept
        {
            if (col != rang::bg::reset) {
                state.bgColor = ansi2attr(static_cast<BYTE>(col) - 40);
            }
            else {
                state.bgColor = defaultState().bgColor;
            }
        }

        inline void setWinSGR(rang::fg col, SGR& state) noexcept
        {
            if (col != rang::fg::reset) {
                state.fgColor = ansi2attr(static_cast<BYTE>(col) - 30);
            }
            else {
                state.fgColor = defaultState().fgColor;
            }
        }

        inline void setWinSGR(rang::bgB col, SGR& state) noexcept
        {
            state.bgColor = (BACKGROUND_INTENSITY >> 4)
                | ansi2attr(static_cast<BYTE>(col) - 100);
        }

        inline void setWinSGR(rang::fgB col, SGR& state) noexcept
        {
            state.fgColor
                = FOREGROUND_INTENSITY | ansi2attr(static_cast<BYTE>(col) - 90);
        }

        inline void setWinSGR(rang::style style, SGR& state) noexcept
        {
            switch (style) {
            case rang::style::reset: state = defaultState(); break;
            case rang::style::bold: state.bold = FOREGROUND_INTENSITY; break;
            case rang::style::underline:
            case rang::style::blink:
                state.underline = BACKGROUND_INTENSITY;
                break;
            case rang::style::reversed: state.inverse = TRUE; break;
            case rang::style::conceal: state.conceal = TRUE; break;
            default: break;
            }
        }

        inline SGR& current_state() noexcept
        {
            static SGR state = defaultState();
            return state;
        }

        inline WORD SGR2Attr(const SGR& state) noexcept
        {
            WORD attrib = 0;
            if (state.conceal) {
                if (state.inverse) {
                    attrib = (state.fgColor << 4) | state.fgColor;
                    if (state.bold)
                        attrib |= FOREGROUND_INTENSITY | BACKGROUND_INTENSITY;
                }
                else {
                    attrib = (state.bgColor << 4) | state.bgColor;
                    if (state.underline)
                        attrib |= FOREGROUND_INTENSITY | BACKGROUND_INTENSITY;
                }
            }
            else if (state.inverse) {
                attrib = (state.fgColor << 4) | state.bgColor;
                if (state.bold) attrib |= BACKGROUND_INTENSITY;
                if (state.underline) attrib |= FOREGROUND_INTENSITY;
            }
            else {
                attrib = state.fgColor | (state.bgColor << 4) | state.bold
                    | state.underline;
            }
            return attrib;
        }

        template <typename T>
        inline void setWinColorAnsi(std::ostream& os, T const value)
        {
            os << "\033[" << static_cast<int>(value) << "m";
        }

        template <typename T>
        inline void setWinColorNative(std::ostream& os, T const value)
        {
            const HANDLE h = getConsoleHandle(os.rdbuf());
            if (h != INVALID_HANDLE_VALUE) {
                setWinSGR(value, current_state());
                // Out all buffered text to console with previous settings:
                os.flush();
                SetConsoleTextAttribute(h, SGR2Attr(current_state()));
            }
        }

        template <typename T>
        inline enableStd<T> setColor(std::ostream& os, T const value)
        {
            if (winTermMode() == winTerm::Auto) {
                if (supportsAnsi(os.rdbuf())) {
                    setWinColorAnsi(os, value);
                }
                else {
                    setWinColorNative(os, value);
                }
            }
            else if (winTermMode() == winTerm::Ansi) {
                setWinColorAnsi(os, value);
            }
            else {
                setWinColorNative(os, value);
            }
            return os;
        }
#else
        template <typename T>
        inline enableStd<T> setColor(std::ostream& os, T const value)
        {
            return os << "\033[" << static_cast<int>(value) << "m";
        }
#endif
    }  // namespace rang_implementation

    template <typename T>
    inline rang_implementation::enableStd<T> operator<<(std::ostream& os,
        const T value)
    {
        const control option = rang_implementation::controlMode();
        switch (option) {
        case control::Auto:
            return rang_implementation::supportsColor()
                && rang_implementation::isTerminal(os.rdbuf())
                ? rang_implementation::setColor(os, value)
                : os;
        case control::Force: return rang_implementation::setColor(os, value);
        default: return os;
        }
    }

    inline void setWinTermMode(const rang::winTerm value) noexcept
    {
        rang_implementation::winTermMode() = value;
    }

    inline void setControlMode(const control value) noexcept
    {
        rang_implementation::controlMode() = value;
    }

}  // namespace rang

#undef OS_LINUX
#undef OS_WIN
#undef OS_MAC

#endif /* ifndef RANG_DOT_HPP */